package com.smartwebx.ha.dal.util;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.math3.stat.StatUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import com.foundationdb.sql.StandardException;
import com.google.common.base.CharMatcher;
import com.google.common.base.Objects;
import com.google.common.base.Predicates;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.smartwebx.ha.dal.config.StatGroupThreadLocal;
import com.smartwebx.ha.dal.handler.InQueryHandler;

@Intercepts(@Signature(type = Executor.class, method = "query", args = {
		MappedStatement.class, Object.class, RowBounds.class,
		ResultHandler.class }))
public class MybatisInQueryInterceptor2 implements Interceptor {
	public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
	public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();

	public static final MetaObject NULL_META_OBJECT = MetaObject.forObject(
			NullObject.class, DEFAULT_OBJECT_FACTORY,
			DEFAULT_OBJECT_WRAPPER_FACTORY);

	enum DoType {
		InQuery, StcdFilter;
	}

	private static class NullObject {
	}

	/**
	 * 反射对象，增加对低版本Mybatis的支持
	 * 
	 * @param object
	 * @return
	 */
	public static MetaObject forObject(Object object) {
		return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY,
				DEFAULT_OBJECT_WRAPPER_FACTORY);
	}

	private static final String BOUND_SQL = "sqlSource.boundSql.sql";

	private static final List<ResultMapping> EMPTY_RESULTMAPPING = new ArrayList<ResultMapping>(
			0);

	@SuppressWarnings("unchecked")
	private String buildInQuery(String sql,
			Entry<String, Integer> paramNameIndexMap, Object paramObj) {
		String paramName = paramNameIndexMap.getKey();
		Integer inQueryIndex = paramNameIndexMap.getValue();

		ParamMap<Object> paramMaps = ((MapperMethod.ParamMap<Object>) paramObj);
		Iterable<Object> paramVal = (Iterable<Object>) paramMaps.get(paramName);

		StringBuffer newSql = new StringBuffer();
		CharMatcher charM = CharMatcher.is('?');

		int i = 0;
		int sumIndex = 0;
		int limitSize = 100;
		while (true) {
			i = charM.indexIn(sql, i + 1);
			if (i == -1)
				break;
			if (sumIndex == inQueryIndex) {
				CharSequence begin = sql.subSequence(0, i);
				CharSequence end = sql.subSequence(i + 1, sql.length());
				int size = Iterables.size(paramVal);
				if (size == 0) {
					newSql.append(begin);
					newSql.append("'NONE'");
					newSql.append(end);
				} else {
					String inQueryParamName = null;
					if (size <= limitSize) {
						newSql.append(begin);
					} else {
						inQueryParamName = this.getfindParamName(
								begin.toString(), newSql);
						newSql.append("(");
					}
					int addCharLen = 0;
					for (int paramValIndex = 0; paramValIndex < size; paramValIndex++) {
						Object val = Iterables.get(paramVal, paramValIndex);

						if (size > limitSize) {
							newSql.append(inQueryParamName + " = '" + val
									+ "' OR ");
							addCharLen = 4;
						} else {
							if (val instanceof String) {
								newSql.append("'" + val + "',");
							} else if (val instanceof Integer) {
								newSql.append(val + ",");
							}
							addCharLen = 1;
						}
					}
					newSql.delete(newSql.length() - addCharLen, newSql.length());
					newSql.append(end);
					break;
				}
			}
			sumIndex++;
		}

		return newSql.toString();
	}
	
	private String builderStcdFilter(String sql) throws StandardException {
		Set<String> statGroups = StatGroupThreadLocal.getStatGroups();
		int haveKey = -1;
		String key = null;
		String groupByKey = "GROUP BY";
		String orderByKey = "ORDER BY";
		String upperSql = StringUtils.upperCase(sql);
		int groupByIndex = StringUtils.indexOf(upperSql, groupByKey);
		int orderByIndex = StringUtils.indexOf(upperSql, orderByKey);
		// 存在group by
		if (groupByIndex >= 0 && orderByIndex >= 0) {
			haveKey = 0;
			if (groupByIndex < orderByIndex) {
				key = groupByKey;
			} else {
				key = orderByKey;
			}
		} else if (groupByIndex >= 0) {
			haveKey = 1;
			key = groupByKey;
		} else if (orderByIndex >= 0) {
			haveKey = 2;
			key = orderByKey;
		}

		StringBuffer newSql = new StringBuffer();
		if (haveKey != -1) {
			String[] sqlSplit = StringUtils.splitByWholeSeparator(sql, key);
			if (sqlSplit.length == 2) {
				newSql.append(sqlSplit[0] + " AND (");
				int count = 0;
				for (String stcd : statGroups) {
					count++;
					newSql.append("STCD='" + stcd + "'");
					if (count != statGroups.size())
						newSql.append(" OR ");
				}
				newSql.append(") " + key);
				newSql.append(" " + sqlSplit[1]);
			} else {
				throw new RuntimeException("SQL 分割异常 SQL = " + sql + " Key = "
						+ key);
			}

		} else {
			newSql.append(sql + " AND (");
			int count = 0;
			for (String stcd : statGroups) {
				count++;
				newSql.append("STCD='" + stcd + "'");
				if (count != statGroups.size())
					newSql.append(" OR ");
			}
			newSql.append(")");
		}

		return newSql.toString();
	}

	private String getfindParamName(String beginStr, StringBuffer newSql) {
		Splitter spaceSpliter = Splitter.on(' ').omitEmptyStrings();
		int findPoint = StringUtils.lastIndexOfAny(beginStr, new String[] {
				"WHERE", "AND", "OR" });
		String keySubBeforeStr = StringUtils.substring(beginStr, findPoint);
		String keySubAfterStr = StringUtils.substring(beginStr, 0, findPoint);
		Iterable<String> spliterResult = spaceSpliter.split(keySubBeforeStr);
		String findKey = Iterables.get(spliterResult, 0);
		newSql.append(keySubAfterStr + " " + findKey + " ");
		String paramName = Iterables.get(spliterResult, 1);
		return paramName;
	}

	private Entry<String, Integer> checkIsInQuery(
			List<ParameterMapping> paramMappings) {
		Entry<String, Integer> paramNameIndexMap = null;

		for (int i = 0; i < paramMappings.size(); i++) {
			ParameterMapping paramM = paramMappings.get(i);
			if (paramM.getTypeHandler() instanceof InQueryHandler) {
				paramNameIndexMap = Maps
						.immutableEntry(paramM.getProperty(), i);
				break;
			}
		}

		return paramNameIndexMap;
	}

	/**
	 * Mybatis拦截器方法
	 * 
	 * @param invocation
	 * @return
	 * @throws Throwable
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		final Object[] args = invocation.getArgs();
		MappedStatement ms = (MappedStatement) args[0];
		String id = ms.getId();
		
		Object parameterObject = args[1];
		Object parameterObjectClone = ObjectUtils.clone(parameterObject);
		BoundSql boundSql = ms.getBoundSql(parameterObjectClone);
		List<String> keysName = Lists.newArrayList("startHour", "endHour",
				"startDate", "endDate", "datePoint");
		List<ParameterMapping> parameterMap = boundSql.getParameterMappings();
		
		// 所有实时包下面且含有参数的方法都会被代理
		if(StringUtils.indexOf(id, "realtime.dao")==-1 || parameterMap.isEmpty()){
			return invocation.proceed();
		}
		
		MapperMethod.ParamMap<Object> paramterObj = (MapperMethod.ParamMap<Object>) boundSql
				.getParameterObject();
		
		Calendar truncateNowCal = Calendar.getInstance();
		int nowHour = truncateNowCal.get(Calendar.HOUR);
		if (truncateNowCal.get(Calendar.MINUTE) >= 10) {
			truncateNowCal = DateUtils.truncate(truncateNowCal, Calendar.HOUR);
		} else {
			truncateNowCal = DateUtils.truncate(truncateNowCal, Calendar.HOUR);
			truncateNowCal.add(Calendar.HOUR, -1);
		}
		Date truncateNow = truncateNowCal.getTime();
		
		Map<String, List<Date>> queryMap = Maps.newHashMap();
		int queryType = 0;// 1 日期范围查询，2 时间范围查询，3 时间点查询
		for (ParameterMapping param : parameterMap) {
			String property = param.getProperty();
			if (keysName.contains(property)) {
				switch (property) {
				case "endDate":
					queryType = 1;
					break;
				case "endHour":
					queryType = 2;
					break;
				case "datePoint":
					queryType = 3;
					break;
				}
			}
		}
		Boolean isInQuery = false;
		Boolean defCache = ms.isUseCache();
		Object result = null;
		
		if(id.matches(".*InQuery$")){
			isInQuery = true;
		}
		
		Boolean noCacheQuery = false;
		if (queryType != 0) {
			Date v1 = null, v2 = null;
			if (queryType == 1) {
				v1 = (Date) paramterObj.get("startDate");
				v2 = (Date) paramterObj.get("endDate");
			} else if (queryType == 2) {
				v1 = (Date) paramterObj.get("startHour");
				v2 = (Date) paramterObj.get("endHour");
			} else if (queryType == 3) {
				v1 = (Date) paramterObj.get("datePoint");
				v2 = (Date) paramterObj.get("datePoint");
			}

			if (v1.compareTo(v2) == 0 && truncateNow.compareTo(v2) <= 0) { // 开始结束时间一样且结束时间大于当前时间
				noCacheQuery = true;
			} else if (v1.compareTo(v2) < 0 && truncateNow.compareTo(v2) < 0) {// 结束时间大于开始时间且结束时间大于当前时间
				if(queryType == 1 && nowHour == 8){
					noCacheQuery = true;
				}else if(queryType == 1 && nowHour != 8){
					noCacheQuery = false;
				}else{
					queryMap.put("cache", Lists.newArrayList(v1, truncateNow));
					queryMap.put("noCache", Lists.newArrayList(truncateNow, v2));
				}
			}
			
			ResultMap resultMap = ms.getResultMaps().get(0);
			Class<?> resultType = resultMap.getType();
			if (queryMap.isEmpty()) {
				if(noCacheQuery || isInQuery){
					result = this.query(args, invocation,false,isInQuery);
				}else{
					result = invocation.proceed();
				}
			} else {
				for(String queryKey : queryMap.keySet()){
					Boolean cache = false;
					if(Objects.equal("cache", queryKey)){
						cache = defCache;
					}else if(Objects.equal("noCache", queryKey)){
						cache = false;
					}
					
					Date start = queryMap.get(queryKey).get(0);
					Date end = queryMap.get(queryKey).get(1);
					if(queryType == 1){// 日期
						paramterObj.put("startDate", start);
						paramterObj.put("endDate", end);
					}else if(queryType == 2){ // 时间
						paramterObj.put("startHour", start);
						paramterObj.put("endHour", end);
					}
					
					if(result == null){
						result = this.query(args, invocation,cache,isInQuery);
					}else{
						Object temResult = this.query(args, invocation,cache,isInQuery);
						
						if(result instanceof List && temResult instanceof List){
							//List t1 = (List)temResult;
							List t2 = (List)result;
							//t2.addAll(t1);
						}else if(result instanceof Map && temResult instanceof Map){
							Map t1 = (Map)temResult;
							Map t2 = (Map)result;
							t2.putAll(t1);
						}
					}
				}
				if(resultType == Double.class){
					List resultList = (List)result;
					Iterables.removeIf(resultList, Predicates.isNull());
					
					if(resultList.isEmpty()){
						result = Lists.newArrayList(0d);
					}else{
						Double[] tt = Iterables.toArray(resultList,
								Double.class);
						double[] numArr = ArrayUtils.toPrimitive(tt);
						double sumNum = StatUtils.sum(numArr);
						result = Lists.newArrayList(sumNum);
					}
				}
			}
			
		}else{
			result = this.query(args, invocation,defCache,isInQuery);
		}
		
		
		
		// 得到处理结果
		return result;
	}

	private Object query(Object[] args, Invocation invocation,Boolean isCache,Boolean isInQuery)
			throws InvocationTargetException, IllegalAccessException {
		Object result = null;
		
		MappedStatement ms = (MappedStatement) args[0];
		
		Object parameterObject = args[1];
		Object parameterObjectClone = ObjectUtils.clone(parameterObject);
		BoundSql boundSql = ms.getBoundSql(parameterObjectClone);
		// 创建一个新的MappedStatement
		MappedStatement qs = this.newMappedStatement(ms, new BoundSqlSqlSource(
				boundSql),isCache);
		// 将参数中的MappedStatement替换为新的qs，防止并发异常
		args[0] = qs;
		
		if(isInQuery){
			MetaObject msObject = forObject(qs);
			String sql = (String) msObject.getValue(BOUND_SQL);
			Entry<String, Integer> paramNameIndexMap = this
					.checkIsInQuery(boundSql.getParameterMappings());
			sql = this.buildInQuery(sql, paramNameIndexMap, parameterObject);
			msObject.setValue(BOUND_SQL, sql);
			result = invocation.proceed();
		}else{
			result = invocation.proceed();
		}
		args[0] = ms;
		
		return result;
	}

	private class BoundSqlSqlSource implements SqlSource {
		BoundSql boundSql;

		public BoundSqlSqlSource(BoundSql boundSql) {
			this.boundSql = boundSql;
		}

		public BoundSql getBoundSql(Object parameterObject) {
			return boundSql;
		}
	}

	/**
	 * 由于MappedStatement是一个全局共享的对象，因而需要复制一个对象来进行操作，防止并发访问导致错误
	 * 
	 * @param ms
	 * @param newSqlSource
	 * @return
	 */
	private MappedStatement newMappedStatement(MappedStatement ms,
			SqlSource newSqlSource,Boolean isCache) {
		MappedStatement.Builder builder = new MappedStatement.Builder(
				ms.getConfiguration(), ms.getId()+"_InQuery" , newSqlSource,
				ms.getSqlCommandType());
		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		builder.useCache(isCache);
		if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
			StringBuilder keyProperties = new StringBuilder();
			for (String keyProperty : ms.getKeyProperties()) {
				keyProperties.append(keyProperty).append(",");
			}
			keyProperties.delete(keyProperties.length() - 1,
					keyProperties.length());
			builder.keyProperty(keyProperties.toString());
		}
		builder.timeout(ms.getTimeout());
		builder.parameterMap(ms.getParameterMap());
		builder.resultMaps(ms.getResultMaps());
		builder.resultSetType(ms.getResultSetType());
		builder.cache(ms.getCache());
		builder.flushCacheRequired(ms.isFlushCacheRequired());

		MappedStatement qs = builder.build();
		return qs;
	}

	/**
	 * 只拦截Executor
	 * 
	 * @param target
	 * @return
	 */
	@Override
	public Object plugin(Object target) {
		if (target instanceof Executor) {
			return Plugin.wrap(target, this);
		} else {
			return target;
		}
	}

	/**
	 * 设置属性值
	 * 
	 * @param p
	 */
	public void setProperties(Properties p) {
	}

}
